package demisemapping;

import java.awt.Color;
import java.awt.Graphics;
import java.io.File;
import java.io.FileWriter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Scanner;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;

public class Dungeon {
    public static final String FILENAME = "data/dungeon.dat";
    public static final int SIZE = 91;
    public static int TILESIZE = 14;

    public static final int EDGE_NORMAL = 0;
    public static final int EDGE_STAIRS_UP = 1;
    public static final int EDGE_STAIRS_DOWN = 2;
    
    public static final Location MIN_LOCATION = new Location(0, 0, 0);
    public static final Location MAX_LOCATION = new Location(SIZE, SIZE, SIZE);
    
    public static final int FLAG_REACHABLE = 1;
    public static final int FLAG_HIGHLIGHTED = 2;
    
    public static int[][][] grid;
    public static int[][][] flags;
    public static ConcurrentSkipListSet<Edge> adj; // adjacency map
    static {
        grid = new int[SIZE][SIZE][SIZE];
        flags = new int[SIZE][SIZE][SIZE];
        adj = new ConcurrentSkipListSet<Edge>();
        try { load(); } catch (Exception ex) { ex.printStackTrace(); }
    }
    
    public static int getX(int j) {
        return TILESIZE*(j-1);
    }
    
    public static int getX(Location loc) {
        return getX(loc.j);
    }
    
    public static int getY(int i) {
        return TILESIZE*(SIZE-(i-1));
    }
    
    public static int getY(Location loc) {
        return getY(loc.i);
    }
    
    private static void tryAddEdge(Edge newEdge) {
        for (Edge e : adj) {
            if (e.compareTo(newEdge) == 0) return;
        }
        adj.add(newEdge);
    }
    
    public static void load() throws Exception {
        // read map
        Scanner in = new Scanner(new File(FILENAME));
        for (int k=0;k<SIZE;k++) for (int i=0;i<SIZE;i++) for (int j=0;j<SIZE;j++) {
            grid[k][i][j] = in.nextInt();
        }
        // read edges
        if (in.hasNextInt()) {
            int size = in.nextInt();
            for (int i=0;i<size;i++) {
                String s = in.next();
                Location a = new Location(s);
                s = in.next();
                Location b = new Location(s);
                int type = in.nextInt();
                adj.add(new Edge(a, b, type));
            }
        }
        
        // add edge into town
        tryAddEdge(new Edge(new Location("24,3,2"), new Location("24,2,2"), EDGE_NORMAL));
        tryAddEdge(new Edge(new Location("24,2,2"), new Location("24,3,2"), EDGE_NORMAL));
        
        // clean up
        in.close();
    }
    
    public static void save() throws Exception {
        // write map
        FileWriter fw = new FileWriter(new File(FILENAME));
        for (int k=0;k<SIZE;k++) {
            for (int i=0;i<SIZE;i++) for (int j=0;j<SIZE;j++) {
                fw.write(grid[k][i][j] + (j < SIZE-1 ? " " : "\n"));
            }
            fw.write("\n");
        }
        // write edges
        fw.write(String.valueOf(adj.size())+"\n");
        for (Edge e : adj) {
            fw.write(e+"\n");
        }
        fw.write("\n");
        // clean up
        fw.close();
    }
    
    private static boolean flagged(int x, int flag) {
        return (x & flag) != 0;
    }
    
    private static boolean flagged(int k, int i, int j, int flag) {
        return flagged(flags[k][i][j], flag);
    }
    
    public static boolean reachable(int k, int i, int j) {
        return flagged(flags[k][i][j], FLAG_REACHABLE);
    }
    
    public static void drawLevel(Graphics g, Main m, int level) {
        // draw tiles
        for (int i=1;i<SIZE;i++) {
            for (int j=1;j<SIZE;j++) {
                if (grid[level][i][j] != 0) {
                    g.drawImage(Images.WALKABLE, getX(j), getY(i), null);
                }
            }
        }
        
        // draw walls
        final int NORTH = 1;
        final int WEST = 2;
        final int EAST = 4;
        final int SOUTH = 8;
        final int ALL = NORTH | WEST | EAST | SOUTH;
        
        Set<Edge> adjacent = adj.subSet(new Edge(new Location(level, 0, 0), MIN_LOCATION, 0), true,
                                        new Edge(new Location(level, SIZE, SIZE), MAX_LOCATION, 0), true);
        
        byte walls[][] = new byte[SIZE][SIZE];
        for (int i=0;i<SIZE;i++) for (int j=0;j<SIZE;j++) {
            walls[i][j] = ALL;
        }
        
        for (Edge e : adjacent/*adj*/) {
            if (e.a.k != level) break; // continue; // optimize this later!!!!!
            
            walls[e.a.i][e.a.j] &= (~(e.b.i > e.a.i ? NORTH : e.b.i < e.a.i ? SOUTH : e.b.j > e.a.j ? EAST : e.b.j < e.a.j ? WEST : 0));
            walls[e.b.i][e.b.j] &= (~(e.a.i > e.b.i ? NORTH : e.a.i < e.b.i ? SOUTH : e.a.j > e.b.j ? EAST : e.a.j < e.b.j ? WEST : 0));
        }
        
        for (Edge e : adjacent/*adj*/) {
            if (e.a.k != level) break; // continue; // optimize this later!!!!!
            
            // color the edge
//            if (flagged(e.a.k, e.a.i, e.a.j, FLAG_HIGHLIGHTED) &&
//                flagged(e.b.k, e.b.i, e.b.j, FLAG_HIGHLIGHTED)) {
//                g.setColor(Color.YELLOW);
            /*} else*/
            if (flagged(e.b.k, e.b.i, e.b.j, FLAG_REACHABLE)) {
                g.setColor(Color.WHITE);
            } else {
                g.setColor(Color.GRAY);
            }
            
            if ((walls[e.a.i][e.a.j] & NORTH) > 0) {
                g.drawLine(getX(e.a), getY(e.a),
                           getX(e.a)+TILESIZE, getY(e.a));
            } 
            if ((walls[e.a.i][e.a.j] & SOUTH) > 0) {
                g.drawLine(getX(e.a), getY(e.a)+TILESIZE,
                           getX(e.a)+TILESIZE, getY(e.a)+TILESIZE);
            }
            if ((walls[e.a.i][e.a.j] & WEST) > 0) {
                g.drawLine(getX(e.a), getY(e.a),
                           getX(e.a), getY(e.a)+TILESIZE);
            }
            if ((walls[e.a.i][e.a.j] & EAST) > 0) {
                g.drawLine(getX(e.a)+TILESIZE, getY(e.a),
                           getX(e.a)+TILESIZE, getY(e.a)+TILESIZE);
            }
        }

        for (Edge e : adjacent/*adj*/) {
            if (e.a.k != level) break; // continue; // optimize this later!!!!!
            
            // color the edge
            if (flagged(e.a.k, e.a.i, e.a.j, FLAG_HIGHLIGHTED) &&
                flagged(e.b.k, e.b.i, e.b.j, FLAG_HIGHLIGHTED)) {
                g.setColor(Color.YELLOW);
                
                // draw the edge
                g.drawLine(getX(e.a)+TILESIZE/2, getY(e.a)+TILESIZE/2,
                        getX(e.b)+TILESIZE/2, getY(e.b)+TILESIZE/2);
            }// else if (flagged(e.b.k, e.b.i, e.b.j, FLAG_REACHABLE)) {
//                g.setColor(Color.CYAN);
//            } else {
//                g.setColor(Color.GRAY);
//            }
//            
//            // draw the edge
//            g.drawLine(getX(e.a)+TILESIZE/2, getY(e.a)+TILESIZE/2,
//                       getX(e.b)+TILESIZE/2, getY(e.b)+TILESIZE/2);
            
            // draw extra image decorations
            if (e.type == EDGE_STAIRS_DOWN) {
                g.drawImage(Images.STAIRS_DOWN, getX(e.b), getY(e.b), null);
            } else if (e.type == EDGE_STAIRS_UP) {
                g.drawImage(Images.STAIRS_UP, getX(e.a), getY(e.a), null);
            }
        }        
    }
    
    static void deleteEdges(Location loc) {
        System.out.println("Deleting all edges incident to " + loc);
        LinkedList<Edge> toDelete = new LinkedList<Edge>();
        for (Edge e : adj) {
            if (e.a.equals(loc) || e.b.equals(loc)) toDelete.add(e);
        }
        for (Edge e : toDelete) {
            adj.remove(e);
        }
    }
    
    static void mapTile(Location current, Location last) {
        Dungeon.grid[current.k][current.i][current.j] = 1;
        if (current == null || last == null) return;
        if (current.dist(last) == 1) {
            adj.add(new Edge(last, current, EDGE_NORMAL));
            adj.add(new Edge(current, last, EDGE_NORMAL));
        } else if (current.dist(last) == 2 && Math.abs(current.k-last.k) == 1) {
            adj.add(new Edge(last, current,
                    (current.k > last.k ? EDGE_STAIRS_DOWN : EDGE_STAIRS_UP)));
            adj.add(new Edge(current, last,
                    (current.k > last.k ? EDGE_STAIRS_UP: EDGE_STAIRS_DOWN)));
        }
    }
    
    public static void setFlag(int flag, boolean on) {
        for(int i=0;i<SIZE;i++) for(int j=0;j<SIZE;j++) for(int k=0;k<SIZE;k++) {
            if (on) flags[k][i][j] |= flag;
            else flags[k][i][j] &= (~flag);
        }
    }
    
    public static void setFlag(int flag, boolean[][][] on) {
        for(int i=0;i<SIZE;i++) for(int j=0;j<SIZE;j++) for(int k=0;k<SIZE;k++) {
            if (on[k][i][j]) flags[k][i][j] |= flag;
            else flags[k][i][j] &= (~flag);
        }
    }
    
    public static void setFlag(int flag, ArrayList<Location> on) {
        setFlag(flag, false);
        for (int i=0;i<on.size();i++)  {
            flags[on.get(i).k][on.get(i).i][on.get(i).j] |= flag;
        }
    }
    
    static void bfsReachable(Location a) {
        LinkedList<Location> q = new LinkedList<Location>();
        boolean[][][] visited = new boolean[SIZE][SIZE][SIZE];
        q.add(a);
        visited[a.k][a.i][a.j] = true;
        while (q.size() > 0) {
            Location loc = q.remove();
            Set<Edge> adjacent = adj.subSet(new Edge(loc, MIN_LOCATION, 0), true,
                                            new Edge(loc, MAX_LOCATION, 0), true);
            for (Edge e : adjacent) {
                assert(loc.equals(e.a));
                if (visited[e.b.k][e.b.i][e.b.j]) continue;
                visited[e.b.k][e.b.i][e.b.j] = true;
                q.add(e.b);
            }
        }
        // replace reachable flags with visited info
        setFlag(FLAG_REACHABLE, visited);
    }
    
    static ArrayList<Location> bfsPath(Location a, Location b) {
        LinkedList<Location> q = new LinkedList<Location>();
        boolean[][][] visited = new boolean[SIZE][SIZE][SIZE];
        Location[][][] prev = new Location[SIZE][SIZE][SIZE];
        q.add(a);
        visited[a.k][a.i][a.j] = true;
        
        while (q.size() > 0) {
            Location loc = q.remove();
            Set<Edge> adjacent = adj.subSet(new Edge(loc, MIN_LOCATION, 0), true,
                                            new Edge(loc, MAX_LOCATION, 0), true);
            for (Edge e : adjacent) {
                assert(loc.equals(e.a));
                if (visited[e.b.k][e.b.i][e.b.j]) continue;
                visited[e.b.k][e.b.i][e.b.j] = true;
                prev[e.b.k][e.b.i][e.b.j] = loc;
                q.add(e.b);
            }
        }
        
        // if cannot reach b, return null
        if (!visited[b.k][b.i][b.j]) return null;
        
        // trace back from destination to find path to source
        //  building path in the proper direction (can't do this with arraylist)
        LinkedList<Location> temp = new LinkedList<Location>();
        Location x = b;
        while (x != null) {
            temp.addFirst(x);
            x = prev[x.k][x.i][x.j];
        }
        return new ArrayList<Location>(temp);
    }
}
